/* ******************************************************************************
 * Copyright (c) 2006-2012 XMind Ltd. and others.
 * 
 * This file is a part of XMind 3. XMind releases 3 and
 * above are dual-licensed under the Eclipse Public License (EPL),
 * which is available at http://www.eclipse.org/legal/epl-v10.html
 * and the GNU Lesser General Public License (LGPL), 
 * which is available at http://www.gnu.org/licenses/lgpl.html
 * See http://www.xmind.net/license.html for details.
 * 
 * Contributors:
 *     XMind Ltd. - initial API and implementation
 *******************************************************************************/
package org.xmind.ui.richtext;

import org.eclipse.core.commands.ExecutionException;
import org.eclipse.core.commands.operations.IUndoContext;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.ITextInputListener;
import org.eclipse.jface.text.ITextViewer;
import org.eclipse.jface.text.ITextViewerExtension;
import org.eclipse.jface.text.ITextViewerExtension5;
import org.eclipse.jface.text.IUndoManager;
import org.eclipse.jface.text.IUndoManagerExtension;
import org.eclipse.jface.text.Region;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.StyledText;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.events.KeyListener;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.MouseListener;
import org.eclipse.text.undo.DocumentUndoEvent;

public class RichTextViewerUndoManager implements IUndoManager,
        IUndoManagerExtension {

    /**
     * Internal listener to mouse and key events.
     */
    private class KeyAndMouseListener implements MouseListener, KeyListener {

        /*
         * @see MouseListener#mouseDoubleClick
         */
        public void mouseDoubleClick(MouseEvent e) {
        }

        /*
         * If the right mouse button is pressed, the current editing command is
         * closed
         * 
         * @see MouseListener#mouseDown
         */
        public void mouseDown(MouseEvent e) {
            if (e.button == 1)
                commit();
        }

        /*
         * @see MouseListener#mouseUp
         */
        public void mouseUp(MouseEvent e) {
        }

        /*
         * @see KeyListener#keyPressed
         */
        public void keyReleased(KeyEvent e) {
        }

        /*
         * On cursor keys, the current editing command is closed
         * 
         * @see KeyListener#keyPressed
         */
        public void keyPressed(KeyEvent e) {
            switch (e.keyCode) {
            case SWT.ARROW_UP:
            case SWT.ARROW_DOWN:
            case SWT.ARROW_LEFT:
            case SWT.ARROW_RIGHT:
                commit();
                break;
            }
        }
    }

    /**
     * Internal text input listener.
     */
    private class TextInputListener implements ITextInputListener {

        /*
         * @see org.eclipse.jface.text.ITextInputListener#inputDocumentAboutToBeChanged(org.eclipse.jface.text.IDocument,
         *      org.eclipse.jface.text.IDocument)
         */
        public void inputDocumentAboutToBeChanged(IDocument oldInput,
                IDocument newInput) {
            disconnectDocumentUndoManager();
        }

        /*
         * @see org.eclipse.jface.text.ITextInputListener#inputDocumentChanged(org.eclipse.jface.text.IDocument,
         *      org.eclipse.jface.text.IDocument)
         */
        public void inputDocumentChanged(IDocument oldInput, IDocument newInput) {
            connectDocumentUndoManager(newInput);
        }
    }

    /**
     * Internal document undo listener.
     */
    private class RichDocumentUndoListener implements IRichDocumentUndoListener {

        /*
         * @see org.eclipse.jface.text.IDocumentUndoListener#documentUndoNotification(DocumentUndoEvent)
         */
        public void documentUndoNotification(RichDocumentUndoEvent event) {
            if (!isConnected())
                return;

            int eventType = event.getEventType();
            if (((eventType & DocumentUndoEvent.ABOUT_TO_UNDO) != 0)
                    || ((eventType & DocumentUndoEvent.ABOUT_TO_REDO) != 0)) {
                if (event.isCompound()) {
                    ITextViewerExtension extension = null;
                    if (fTextViewer instanceof ITextViewerExtension)
                        extension = (ITextViewerExtension) fTextViewer;

                    if (extension != null)
                        extension.setRedraw(false);
                }
                // ATTN: commended for test
//                fTextViewer.getTextWidget().getDisplay().syncExec( new Runnable() {
//                    public void run() {
//                        if ( fTextViewer instanceof TextViewer )
//                            ( (TextViewer) fTextViewer ).ignoreAutoEditStrategies( true );
//                    }
//                } );

            } else if (((eventType & DocumentUndoEvent.UNDONE) != 0)
                    || ((eventType & DocumentUndoEvent.REDONE) != 0)) {
//                fTextViewer.getTextWidget().getDisplay().syncExec( new Runnable() {
//                    public void run() {
//                        if ( fTextViewer instanceof TextViewer )
//                            ( (TextViewer) fTextViewer ).ignoreAutoEditStrategies( false );
//                    }
//                } );
                if (event.isCompound()) {
                    ITextViewerExtension extension = null;
                    if (fTextViewer instanceof ITextViewerExtension)
                        extension = (ITextViewerExtension) fTextViewer;

                    if (extension != null)
                        extension.setRedraw(true);
                }

                // Reveal the change if this manager's viewer has the focus.
                if (fTextViewer != null) {
                    StyledText widget = fTextViewer.getTextWidget();
                    if (widget != null && !widget.isDisposed()
                            && (widget.isFocusControl()))// ||
                        // fTextViewer.getTextWidget()
                        // == control))
                        selectAndReveal(event.getOffset(),
                                event.getText() == null ? 0 : event.getText()
                                        .length());
                }
            }
        }

    }

    /** The internal key and mouse event listener */
    private KeyAndMouseListener fKeyAndMouseListener;
    /** The internal text input listener */
    private TextInputListener fTextInputListener;

    /** The text viewer the undo manager is connected to */
    private ITextViewer fTextViewer;

    /** The undo level */
    private int fUndoLevel;

    /** The document undo manager that is active. */
    private IRichDocumentUndoManager fDocumentUndoManager;

    /** The document that is active. */
    private IRichDocument fDocument;

    /** The document undo listener */
    private IRichDocumentUndoListener fDocumentUndoListener;

    /**
     * Creates a new undo manager who remembers the specified number of edit
     * commands.
     * 
     * @param undoLevel
     *            the length of this manager's history
     */
    public RichTextViewerUndoManager(int undoLevel) {
        fUndoLevel = undoLevel;
    }

    /**
     * Returns whether this undo manager is connected to a text viewer.
     * 
     * @return <code>true</code> if connected, <code>false</code> otherwise
     */
    private boolean isConnected() {
        return fTextViewer != null && fDocumentUndoManager != null;
    }

    public IRichDocumentUndoManager getDocumentUndoManager() {
        return fDocumentUndoManager;
    }

    /*
     * @see IUndoManager#beginCompoundChange
     */
    public void beginCompoundChange() {
        if (isConnected()) {
            fDocumentUndoManager.beginCompoundChange();
            //inCompound = true;
        }
    }

    /*
     * @see IUndoManager#endCompoundChange
     */
    public void endCompoundChange() {
        if (isConnected()) {
            fDocumentUndoManager.endCompoundChange();
            //inCompound = false;
        }
    }

    /**
     * Registers all necessary listeners with the text viewer.
     */
    private void addListeners() {
        StyledText text = fTextViewer.getTextWidget();
        if (text != null) {
            fKeyAndMouseListener = new KeyAndMouseListener();
            text.addMouseListener(fKeyAndMouseListener);
            text.addKeyListener(fKeyAndMouseListener);
            fTextInputListener = new TextInputListener();
            fTextViewer.addTextInputListener(fTextInputListener);
        }
    }

    /**
     * Unregister all previously installed listeners from the text viewer.
     */
    private void removeListeners() {
        StyledText text = fTextViewer.getTextWidget();
        if (text != null) {
            if (fKeyAndMouseListener != null) {
                text.removeMouseListener(fKeyAndMouseListener);
                text.removeKeyListener(fKeyAndMouseListener);
                fKeyAndMouseListener = null;
            }
            if (fTextInputListener != null) {
                fTextViewer.removeTextInputListener(fTextInputListener);
                fTextInputListener = null;
            }
        }
    }

//    /**
//     * Shows the given exception in an error dialog.
//     * 
//     * @param title
//     *            the dialog title
//     * @param ex
//     *            the exception
//     */
//    private void openErrorDialog(final String title, final Exception ex) {
//        Shell shell = null;
//        if (isConnected()) {
//            StyledText st = fTextViewer.getTextWidget();
//            if (st != null && !st.isDisposed())
//                shell = st.getShell();
//        }
//        if (Display.getCurrent() != null)
//            MessageDialog.openError(shell, title, ex.getLocalizedMessage());
//        else {
//            Display display;
//            final Shell finalShell = shell;
//            if (finalShell != null)
//                display = finalShell.getDisplay();
//            else
//                display = Display.getDefault();
//            display.syncExec(new Runnable() {
//                public void run() {
//                    MessageDialog.openError(finalShell, title, ex
//                            .getLocalizedMessage());
//                }
//            });
//        }
//    }

    /*
     * @see org.eclipse.jface.text.IUndoManager#setMaximalUndoLevel(int)
     */
    public void setMaximalUndoLevel(int undoLevel) {
        fUndoLevel = Math.max(0, undoLevel);
        if (isConnected()) {
            fDocumentUndoManager.setMaximalUndoLevel(fUndoLevel);
        }
    }

    /*
     * @see org.eclipse.jface.text.IUndoManager#connect(org.eclipse.jface.text.ITextViewer)
     */
    public void connect(ITextViewer textViewer) {
        if (fTextViewer == null && textViewer != null) {
            fTextViewer = textViewer;
            addListeners();
        }
        IDocument doc = fTextViewer.getDocument();
        connectDocumentUndoManager(doc);
    }

    /*
     * @see org.eclipse.jface.text.IUndoManager#disconnect()
     */
    public void disconnect() {
        if (fTextViewer != null) {
            removeListeners();
            fTextViewer = null;
        }
        disconnectDocumentUndoManager();
    }

    /*
     * @see org.eclipse.jface.text.IUndoManager#reset()
     */
    public void reset() {
        if (isConnected()) {
            fDocumentUndoManager.reset();
        }

    }

    /*
     * @see org.eclipse.jface.text.IUndoManager#redoable()
     */
    public boolean redoable() {
        if (isConnected()) {
            return fDocumentUndoManager.redoable();
        }
        return false;
    }

    /*
     * @see org.eclipse.jface.text.IUndoManager#undoable()
     */
    public boolean undoable() {
        if (isConnected())
            return fDocumentUndoManager.undoable();
        return false;
    }

    /*
     * @see org.eclipse.jface.text.IUndoManager#redo()
     */
    public void redo() {
        if (isConnected()) {
            try {
                fDocumentUndoManager.redo();
            } catch (ExecutionException ex) {
                // openErrorDialog(JFaceTextMessages.getString("DefaultUndoManager.error.redoFailed.title"),
                // ex); //$NON-NLS-1$
            }
        }
    }

    /*
     * @see org.eclipse.jface.text.IUndoManager#undo()
     */
    public void undo() {
        if (isConnected()) {
            try {
                fDocumentUndoManager.undo();
            } catch (ExecutionException ex) {
                // openErrorDialog(JFaceTextMessages.getString("DefaultUndoManager.error.undoFailed.title"),
                // ex); //$NON-NLS-1$
            }
        }
    }

    /**
     * Selects and reveals the specified range.
     * 
     * @param offset
     *            the offset of the range
     * @param length
     *            the length of the range
     */
    private void selectAndReveal(int offset, int length) {
        if (fTextViewer instanceof ITextViewerExtension5) {
            ITextViewerExtension5 extension = (ITextViewerExtension5) fTextViewer;
            extension.exposeModelRange(new Region(offset, length));
        } else if (!fTextViewer.overlapsWithVisibleRegion(offset, length))
            fTextViewer.resetVisibleRegion();

        fTextViewer.setSelectedRange(offset, length);
        fTextViewer.revealRange(offset, length);
    }

    /*
     * @see org.eclipse.jface.text.IUndoManagerExtension#getUndoContext()
     */
    public IUndoContext getUndoContext() {
        if (isConnected()) {
            return fDocumentUndoManager.getUndoContext();
        }
        return null;
    }

    private void connectDocumentUndoManager(IDocument document) {
        disconnectDocumentUndoManager();
        if (document != null && document instanceof IRichDocument) {
            fDocument = (IRichDocument) document;
            RichDocumentUndoManagerRegistry.connect(fDocument);
            fDocumentUndoManager = RichDocumentUndoManagerRegistry
                    .getDocumentUndoManager(fDocument);
            fDocumentUndoManager.connect(this);
            setMaximalUndoLevel(fUndoLevel);
            fDocumentUndoListener = new RichDocumentUndoListener();
            fDocumentUndoManager.addDocumentUndoListener(fDocumentUndoListener);
        } else {
            fDocument = null;
        }
    }

    private void disconnectDocumentUndoManager() {
        if (fDocumentUndoManager != null) {
            fDocumentUndoManager.disconnect(this);
            RichDocumentUndoManagerRegistry.disconnect(fDocument);
            fDocumentUndoManager
                    .removeDocumentUndoListener(fDocumentUndoListener);
            fDocumentUndoListener = null;
            fDocumentUndoManager = null;
        }
    }

    public void commit() {
        if (isConnected()) {
            fDocumentUndoManager.commit();
        }
    }

}